﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Xml;
using HtmlAgilityPack;
using System.IO;

namespace craigsPack
{
    public sealed class CraigsListProvider : IWebProvider
    {
        public event EventHandler<SearchResultsEventArgs> SearchComplete;
        public event EventHandler CurrentPageChanged;
        public event EventHandler<SearchResultsEventArgs> FoundResults;

        private readonly WebClient webClient = null;
        private readonly HtmlDocument htmlDocument = null;
        private readonly HashSet<string> cache = null;
        private readonly CraigsListProviderSettings providerSettings = null;

        private string currentPage = null;
        private string currentUrl = null;
        private int pageCounter = 0;

        private CraigsListUrlBuilder urlBuilder = null;
        private CraigsListSearchSettings searchSettings = null;

        /// <summary>
        /// Creates a custom web provider for craigslist with specified settings
        /// </summary>
        /// <param name="providerSettings"></param>
        public CraigsListProvider(CraigsListProviderSettings providerSettings)
        {
            webClient = new WebClient();
            htmlDocument = new HtmlDocument();
            cache = new HashSet<string>();

            this.providerSettings = providerSettings;
        }
        /// <summary>
        /// Creates a custom web provider with default settings
        /// </summary>
        public CraigsListProvider()
            :this(new CraigsListProviderSettings())
        {

        }
        /// <summary>
        /// Searches craigslist using the custom web provider and search settings
        /// </summary>
        /// <param name="searchSettings">the settings used for searching</param>
        public void Search(WebProviderSearchSettings searchSettings)
        {
            this.searchSettings = (CraigsListSearchSettings)searchSettings;
            SearchSettings = this.searchSettings;

            if (providerSettings.EmptyCache || cache.Count > 1000)
                cache.Clear();

            switch (((CraigsListSearchSettings)searchSettings).Category)
            {
                case CraigsListCategory.Services:
                    break;
                case CraigsListCategory.Jobs:
                    urlBuilder = CraigsListUrlBuilder.Create(CraigsListCategory.Jobs);
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.TELE] = this.searchSettings.IsTeleCommuting;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.CONTRACT] = this.searchSettings.IsContract;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.INTERN] = this.searchSettings.IsInternship;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.PARTTIME] = this.searchSettings.IsPartTime;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.NONPROFIT] = this.searchSettings.IsNonProfit;
                    break;
                case CraigsListCategory.Sale:
                    urlBuilder = CraigsListUrlBuilder.Create(CraigsListCategory.Sale);
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.MINPRICE] = this.searchSettings.MinPrice;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.MAXPRICE] = this.searchSettings.MaxPrice;
                    break;
                case CraigsListCategory.Housing:
                    urlBuilder = CraigsListUrlBuilder.Create(CraigsListCategory.Housing);
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.MINPRICE] = this.searchSettings.MinPrice;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.MAXPRICE] = this.searchSettings.MaxPrice;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.BED] = this.searchSettings.Bedrooms;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.DOGS] = this.searchSettings.AllowDogs;
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.CATS] = this.searchSettings.AllowCats;
                    break;
                case CraigsListCategory.Gigs:
                    urlBuilder = CraigsListUrlBuilder.Create(CraigsListCategory.Gigs);
                    urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.ISPAY] = this.searchSettings.IsPay;
                    break;
                default:
                    break;
            }
            urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.AREAID] = "";
            urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.SUBID] = "";
            urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.HASPIC] = this.searchSettings.HasPics;
            urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.SEARCHTYPE] = this.searchSettings.SearchEntirePost;

            urlBuilder.Append(CraigsListUrlBuilder.GetQueryStringCategory(this.searchSettings.Category));
            
            HashSet<string> items = new HashSet<string>();
            int lastLen = 0;

            foreach (CityArea item in ((CraigsListSearchSettings)searchSettings).Cities)
            {
                urlBuilder.SubDomain = item.Name;

                foreach (string keyword in (((CraigsListSearchSettings)searchSettings).Keywords))
                {
                    for (int i = 1; i <= providerSettings.MaxPages; i++)
                    {
                        urlBuilder.QueryBuilder["query"] = keyword;
                        ((IWebProvider)this).GotoUrl();
                        var rows = htmlDocument.DocumentNode.FindTags("p",o => o.Attributes["class"].Value == "row" && IsValidDate(o.GetDatePosted()) && !cache.Contains(o.GetTitle()));

                        lastLen = items.Count;
                        foreach (var item2 in rows)
                            items.Add(item2.OuterHtml);

                        if (FoundResults != null && rows.Length > 0 && lastLen < items.Count)
                            FoundResults(this, new SearchResultsEventArgs(new KeyValuePair<string, CraigsListItemInfo[]>(keyword, GetItemInfo(items.Take(items.Count - rows.Length)))));

                        //Dont Move Line Otherwise The Rows Will Be Gone Before It Stores In The Array Items
                        cache.AddRange(rows.Select(o => o.GetTitle()));

                        if (cache.Count >= providerSettings.CacheLimit || cache.Count > 1000)
                            cache.Clear();

                        if (items.Count >= providerSettings.MaxReturns)
                            break;

                        if (!htmlDocument.DocumentNode.HasMorePages())
                        {
                            urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.PAGE] = 0;
                            pageCounter = 0;
                            break;
                        }
                        GotoNextPage();
                    }
                   

                    if (items.Count >= providerSettings.MaxReturns)
                        break;
                }
            }

            if (SearchComplete != null)
                SearchComplete(this, new SearchResultsEventArgs(GetItemInfo(items)));
        }
        /// <summary>
        /// Checks whether a date is greater or less than the date specified in the search settings
        /// </summary>
        /// <param name="date">date of ad</param>
        /// <returns></returns>
        private bool IsValidDate(WeakDate date)
        {
            return date.Month >= searchSettings.LatestDate.Month && date.Day >= searchSettings.LatestDate.Day;
        }
        /// <summary>
        /// Returns all of the html code into a <c>CraigsListItemInfo</c>
        /// </summary>
        /// <param name="nodes">html nodes</param>
        /// <returns></returns>
        private CraigsListItemInfo[] GetItemInfo(IEnumerable<string> nodes)
        {
            List<CraigsListItemInfo> items = new List<CraigsListItemInfo>();
            HtmlNode item = null;
            foreach (var item2 in nodes)
            {
                item = HtmlNode.CreateNode(item2.Replace("\t","").Replace("\n","").Replace("\r",""));
                    Uri link = item.GetLink();
                    if (searchSettings.Contains("ctoOnly") && (bool)searchSettings["ctoOnly"] && link.AbsolutePath.Split('/').Any(o => o == "ctd"))
                        continue;
                    items.Add(new CraigsListItemInfo(item.GetPrice(), -1, item.GetLink(), null, item.GetWeakCity(), null, null, item.GetDatePosted()));
            }
            
            return items.ToArray();
        }
        /// <summary>
        /// Moves the current url up one page
        /// </summary>
        private void GotoNextPage()
        {
            pageCounter += 100;
            urlBuilder.QueryBuilder[CraigsListQueryStringBuilder.PAGE] = pageCounter.ToString();
        }
        void IWebProvider.GotoUrl()
        {
            currentUrl = urlBuilder.Url;
            currentPage = webClient.DownloadString(currentUrl);
            htmlDocument.LoadHtml(currentPage);

            if (CurrentPageChanged != null)
                CurrentPageChanged(this, EventArgs.Empty);
        }

        public string CurrentUrl
        {
            get { return currentUrl; }
        }
        public string Name
        {
            get { return "CraigsList"; }
        }
        public UrlBuilder UrlBuilder
        {
            get { return urlBuilder; }
        }
        public string CurrentPage
        {
            get { return currentPage; }
        }
        public WebProviderSearchSettings SearchSettings { get; private set; }
    }
}
